﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Windows.Documents;
using System.Windows;
using System.Windows.Media;
using System.Windows.Controls;
using System.Windows.Media.TextFormatting;
using SmartPaginator.TextFormatting;
using System.Reflection;
using SmartPaginator.Elements;
using SmartPaginator.Extensions;

namespace SmartPaginator
{
    public class SmartDocumentPaginator : DocumentPaginator, IElementFactory
    {

        #region Constructor

        public SmartDocumentPaginator(FlowDocument document)
        { 
            Pages = new List<DocumentPage>();

            Document = document;
            InnerPaginator = ((IDocumentPaginatorSource)document).DocumentPaginator;
            PageSize = new Size(21 * DPC, 29.7 * DPC); // A4
            

            // When the margin is not specified, follow the default margin for the document. Note that this value is obtained through calling an 
            // internal method on FlowDocument (Note: why did Microsoft have to make it internal????)
            var method = typeof(FlowDocument).GetMethod("ComputePageMargin", BindingFlags.Instance | BindingFlags.NonPublic);
            Margin = (Thickness)method.Invoke(document, null);
        }

        public SmartDocumentPaginator(FlowDocument document, Size pageSize, Thickness margin)
            : this(document)
        {
            PageSize = pageSize;
            Margin = margin;
        }

        #endregion

        #region Constants

        // "Dots per centimeter"
        private const double DPC = 96 / 2.54;

        #endregion

        #region New Properties

        protected FlowDocument Document { get; private set; }

        protected DocumentPaginator InnerPaginator { get; private set; }

        protected List<DocumentPage> Pages { get; private set; }

        public Thickness Margin
        {
            get { return _Margin; }
            set 
            { 
                _Margin = value;
                RecalculateContentSize();
                OnPagesChanged(new PagesChangedEventArgs(0, PageCount));
            }
        }
        private Thickness _Margin;

        public Rect BleedBox{ get; private set; }

        public Rect ContentBox { get; private set; }

        protected void RecalculateContentSize()
        {
            BleedBox = new Rect(PageSize);
            ContentBox = new Rect(PageSize).Subtract(Margin);
        }

        #endregion

        #region Overriden behaviour

        public override IDocumentPaginatorSource Source
        {
            get { return Document; }
        }

        public override Size PageSize
        {
            get { return _PageSize; }
            set
            {
                _PageSize = value;
                RecalculateContentSize();
                OnPagesChanged(new PagesChangedEventArgs(0, PageCount));
            }
        }
        private Size _PageSize;

        public override bool IsPageCountValid
        {
            get { return _IsPageCountValid; }
        }
        private bool _IsPageCountValid;

        public override int PageCount
        {
            get { return Pages.Count; }
        }

        protected override void OnPagesChanged(PagesChangedEventArgs e)
        {
            _IsPageCountValid = false;
            base.OnPagesChanged(e);
        }

        public override DocumentPage GetPage(int pageNumber)
        {
            if (!IsPageCountValid)
                Paginate();

            if (pageNumber >= PageCount)
                return null;

            return Pages[pageNumber];
        }

        #endregion

        #region IElementFactory Members

        public virtual UIElement GetElementForContent(object content)
        {
            if (content is Section)
            {
                return new SectionElement(content as Section, this);
            }
            else if (content is Paragraph)
            {
                return new ParagraphElement(content as Paragraph, this);
            }
            else if (content is List)
            {
                return new ListElement(content as List, this);
            }
            else if (content is ListItem)
            {
                return new ListItemElement(content as ListItem, this);
            }
            else if (content is Table)
            {
                return new TableElement(content as Table, this);
            }

            throw new NotSupportedException(String.Format("SmartDocumentPaginator does not support '{0}'.", content.GetType()));
        }

        #endregion

        #region Paginate Method 

        private void Paginate()
        {
            Pages.Clear();

            // Calculate minimum column width. If not specified, assume 20 times the line height, like Microsoft does
            // http://msdn.microsoft.com/en-us/library/system.windows.documents.flowdocument.columnwidth.aspx
            double columnWidth = Document.ColumnWidth;
            if (double.IsNaN(columnWidth) || double.IsInfinity(columnWidth))
                columnWidth = 20 * Document.GetLineHeight();

            // Calculate the column gap. If not specified, assume 1 times the line height, like Microsoft does
            // http://msdn.microsoft.com/en-us/library/system.windows.documents.flowdocument.columngap.aspx
            double columnGap = Document.ColumnGap;
            if (double.IsNaN(columnGap) || double.IsInfinity(columnGap))
                columnGap = Document.GetLineHeight();

            // Find out how many columns we can fit
            int columns = 1;
            double aux = (columnWidth * columns) + (columnGap * (columns -1));
            double totalWidth = aux;
            while (aux < this.ContentBox.Width)
            {
                totalWidth = aux;
                columns++;
                aux = (columnWidth * columns) + (columnGap * (columns - 1));
            }
            // When we get here, it is because can't fit the columns anymore (i.e. one column too many)
            if (columns > 1)
                columns--;

            // Distribute the remaining space if the column width is flexible
            if (Document.IsColumnWidthFlexible)
            {
                double remainingSpace = this.ContentBox.Width - totalWidth;
                columnWidth += remainingSpace / columns;
            }

            // This will be the size of a column
            Size columnSize = new Size(columnWidth, ContentBox.Height);

            // We need to set the "ContainerSize" attached property, as this may be used by EmbeddedObjects to calculate their size
            SetContainerSize(Document, columnSize);

            // This will create the elements and their structure as if they were all part of a single column.
            // We do this first so we can measure the result.
            var documentElement = Render(Document, columnSize);

            // This will create DocumentPage instances and then go through the elements created above, 
            // cloning them and adding them to the DocumentPages as appropriate.
            var context = new HarvestingContext();
            var pages = new List<PageElement>();
            PageElement pageElement = null;

            var harvestResult = documentElement.Harvest(context, ContentBox.Height, null);
            while (harvestResult.HarvestedElement != null)
            {
                var columnElement= harvestResult.HarvestedElement as PageColumnElement;

                if (pageElement == null)
                {
                    pageElement = new PageElement(columns, columnWidth, columnGap, Document, PageSize, Margin);
                    pages.Add(pageElement);

                    // Update the PaginationInfo
                    if (columnElement.Section != null)
                    {
                        var section = columnElement.Section.Section;
                        if ((section != context.CurrentSection) && SectionExtensions.GetResetPageNumbers(section))
                        {
                            context.CurrentPage = SectionExtensions.GetFirstPageNumber(section);

                            if (context.CurrentSection != null)
                                context.CurrentSectionGroup++;
                        }

                        pageElement.PaginationInfo.PageNumber = context.CurrentPage;
                        pageElement.PaginationInfo.SectionGroupIndex = context.CurrentSectionGroup;


                        if (!context.SectionGroupPageCount.ContainsKey(context.CurrentSectionGroup))
                        {
                            // We don't add (1). This is because if the section started counting at another number, like 7 for example,
                            // we don't want to say "Page 7 of 1".
                            context.SectionGroupPageCount.Add(context.CurrentSectionGroup, context.CurrentPage);
                        }
                        else
                        {
                            context.SectionGroupPageCount[context.CurrentSectionGroup] = context.SectionGroupPageCount[context.CurrentSectionGroup] + 1;
                        }

                        context.CurrentSection = section;
                        context.CurrentPage++;
                    }

                }


                if (pageElement.AddColumn(columnElement))
                {
                    // Was last column, so break the page
                    pageElement = null;
                }
                else if (harvestResult.AddPageBreak)
                {
                    // Was last column, so break the page
                    pageElement = null;
                }


                // Get the next column
                harvestResult = documentElement.Harvest(context, ContentBox.Height, null);
            }

            // Now that we harvested all the pages, we need to update the PaginationInfo.TotalPages property of every page
            // and then render the page 
            foreach (var page in pages)
            {
                if (page.PaginationInfo.SectionGroupIndex != 0)
                    page.PaginationInfo.TotalPages = context.SectionGroupPageCount[page.PaginationInfo.SectionGroupIndex];

                page.Render();
                Pages.Add(new DocumentPage(page, PageSize, BleedBox, ContentBox));
            }

            // Mark page count as valid
            _IsPageCountValid = true;
        }


        private PageColumnElement Render(FlowDocument document, Size columnSize)
        {
            var element = new PageColumnElement(document, columnSize, this);
            element.Render(true);

            return element;
        }

        #endregion

        #region


        #endregion


        #region ContainerSize Property

        protected static readonly DependencyPropertyKey ContainerSizePropertyKey = DependencyProperty.RegisterAttachedReadOnly(
            "ContainerSize",
            typeof(Size),
            typeof(SmartDocumentPaginator),
            new FrameworkPropertyMetadata(new Size(double.NaN, double.NaN)));

        public static readonly DependencyProperty ContainerSizeProperty = ContainerSizePropertyKey.DependencyProperty;


        public static Size GetContainerSize(DependencyObject element)
        {
            var result = (Size)element.GetValue(ContainerSizeProperty);
            double height = result.Height;
            double width = result.Width;

            if (!double.IsNaN(height) && !double.IsNaN(width))
                return result;

            // One of the properties is set to auto, so let's contain by the parent.
            var contentElement = element as FrameworkContentElement;
            if ((contentElement != null) && (contentElement.Parent != null))
            {
                var parentSize = GetContainerSize(contentElement.Parent);

                if (double.IsNaN(height))
                    height = parentSize.Height;

                if (double.IsNaN(width))
                    width = parentSize.Width;
            }

            return new Size(width, height);
        }

        protected static void SetContainerSize(DependencyObject element, Size value)
        {
            element.SetValue(ContainerSizePropertyKey, value);
        }


        #endregion
    }
}
